﻿// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
// -----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.Internal;

namespace System.ComponentModel.Composition
{
    internal static class ConstraintServices
    {
        // NOTE : these are here as Reflection member search is pretty expensive, and we want that to be done once.
        // Also, making these static would cause this class to fail loading if we rename members of ExportDefinition.
        private static readonly PropertyInfo _exportDefinitionContractNameProperty = typeof(ExportDefinition).GetProperty("ContractName");
        private static readonly PropertyInfo _exportDefinitionMetadataProperty = typeof(ExportDefinition).GetProperty("Metadata");
        private static readonly MethodInfo _metadataContainsKeyMethod = typeof(IDictionary<string, object>).GetMethod("ContainsKey");

        public static Expression<Func<ExportDefinition, bool>> CreateConstraint(string contractName)
        {
            return ConstraintServices.CreateConstraint(contractName, (IEnumerable<string>)null);
        }

        public static Expression<Func<ExportDefinition, bool>> CreateConstraint(IEnumerable<string> requiredMetadata)
        {
            ParameterExpression parameter = Expression.Parameter(typeof(ExportDefinition), "exportDefinition");
            Expression metadataConstraintBody = null;

            if (requiredMetadata != null)
            {
                metadataConstraintBody = ConstraintServices.CreateMetadataConstraintBody(requiredMetadata, parameter);
            }

            if (metadataConstraintBody != null)
            {
                return Expression.Lambda<Func<ExportDefinition, bool>>(metadataConstraintBody, parameter);
            }
            
            return null;
        }

        public static Expression<Func<ExportDefinition, bool>> CreateConstraint(string contractName, IEnumerable<string> requiredMetadata)
        {
            ParameterExpression parameter = Expression.Parameter(typeof(ExportDefinition), "exportDefinition");

            Expression contractConstraintBody = ConstraintServices.CreateContractConstraintBody(contractName, parameter);
            Expression metadataConstraintBody = null;

            if (requiredMetadata != null)
            {
                metadataConstraintBody = ConstraintServices.CreateMetadataConstraintBody(requiredMetadata, parameter);
            }

            Expression constraintBody = (metadataConstraintBody != null) ? Expression.AndAlso(contractConstraintBody, metadataConstraintBody) : contractConstraintBody;
            Expression<Func<ExportDefinition, bool>> constraint = Expression.Lambda<Func<ExportDefinition, bool>>(constraintBody, parameter);
            return constraint;
        }

        private static Expression CreateContractConstraintBody(string contractName, ParameterExpression parameter)
        {
            Assumes.NotNull(parameter);

            // export.ContractName=<contract>;
            return Expression.Equal(
                    Expression.Property(parameter, ConstraintServices._exportDefinitionContractNameProperty),
                    Expression.Constant(contractName ?? string.Empty, typeof(string)));
        }

        private static Expression CreateMetadataConstraintBody(IEnumerable<string> requiredMetadata, ParameterExpression parameter)
        {
            Assumes.NotNull(requiredMetadata);
            Assumes.NotNull(parameter);

            Expression body = null;
            foreach (string requiredMetadataItem in requiredMetadata)
            {
                // export.Metadata.ContainsKey(<metadataItem>)
                Expression metadataItemExpression = Expression.Call(
                    Expression.Property(parameter, ConstraintServices._exportDefinitionMetadataProperty),
                    ConstraintServices._metadataContainsKeyMethod,
                    Expression.Constant(requiredMetadataItem));

                body = (body != null) ? Expression.AndAlso(body, metadataItemExpression) : metadataItemExpression;
            }

            return body;
        }

        public static bool TryParseConstraint(Expression<Func<ExportDefinition, bool>> constraint, out string contractName, out IEnumerable<string> requiredMetadata)
        {
            Assumes.NotNull(constraint);

            contractName = null;
            requiredMetadata = null;

            List<string> requiredMetadataList = new List<string>();
            foreach (Expression expression in ConstraintServices.SplitConstraintBody(constraint.Body))
            {
                // First try to parse as a contract, if we don't have one already
                if (contractName == null && ConstraintServices.TryParseExpressionAsContractConstraintBody(expression, constraint.Parameters[0], out contractName))
                {
                    continue;
                }

                // Then try to parse as a required metadata item name
                string requiredMetadataItemName = null;
                if (ConstraintServices.TryParseExpressionAsMetadataConstraintBody(expression, constraint.Parameters[0], out requiredMetadataItemName))
                {
                    requiredMetadataList.Add(requiredMetadataItemName);
                    continue;
                }

                // At this point we don't know what this is, so we need to return false - the parsing failed
                contractName = null;
                return false;
            }

            // ContractName should have been set already, just need to set metadata
            requiredMetadata = requiredMetadataList;
            return true;
        }


        private static IEnumerable<Expression> SplitConstraintBody(Expression expression)
        {
            Assumes.NotNull(expression);

            // The expression we know about should be a set of nested AndAlso's, we
            // need to flatten them into one list. we do this iteratively, as 
            // recursion will create too much of a memory churn.
            Stack<Expression> expressions = new Stack<Expression>();
            expressions.Push(expression);

            while (expressions.Count > 0)
            {
                Expression current = expressions.Pop();
                if (current.NodeType == ExpressionType.AndAlso)
                {
                    BinaryExpression andAlso = (BinaryExpression)current;
                    // Push right first - this preserves the ordering of the expression, which will force
                    // the contract constraint to come up first as the callers are optimized for this form
                    expressions.Push(andAlso.Right);
                    expressions.Push(andAlso.Left);
                    continue;
                }

                yield return current;
            }
        }

        private static bool TryParseExpressionAsContractConstraintBody(Expression expression, Expression parameter, out string contractName)
        {
            Assumes.NotNull(expression, parameter);

            contractName = null;

            // The expression should be an '==' expression
            if (expression.NodeType != ExpressionType.Equal)
            {
                return false;
            }

            BinaryExpression contractConstraintExpression = (BinaryExpression)expression;

            // First try item.ContractName == "Value"
            if (TryParseContractNameFromEqualsExpression(contractConstraintExpression.Left, contractConstraintExpression.Right, parameter, out contractName))
            {
                return true;
            }

            // Then try "Value == item.ContractName
            if (TryParseContractNameFromEqualsExpression(contractConstraintExpression.Right, contractConstraintExpression.Left, parameter, out contractName))
            {
                return true;
            }
           
            return false;
        }

        private static bool TryParseContractNameFromEqualsExpression(Expression left, Expression right, Expression parameter, out string contractName)
        {
            Assumes.NotNull(left, right, parameter);

            contractName = null;

            // The left should be access to property "Contract" applied to the parameter
            MemberExpression targetMember = left as MemberExpression;
            if (targetMember == null)
            {   
                return false;
            }

            if ((targetMember.Member != ConstraintServices._exportDefinitionContractNameProperty) || (targetMember.Expression != parameter))
            {
                return false;
            }

            // Right should a constant expression containing the contract name
            ConstantExpression contractNameConstant = right as ConstantExpression;
            if (contractNameConstant == null)
            {   
                return false;
            }

            if (!TryParseStringConstant(contractNameConstant, out contractName))
            {
                return false;
            }

            return true;
        }

        private static bool TryParseExpressionAsMetadataConstraintBody(Expression expression, Expression parameter, out string requiredMetadataName)
        {
            Assumes.NotNull(expression, parameter);

            requiredMetadataName = null;

            // Should be a call to IDictionary.ContainsKey() on "Metadata" property applied to the parameter            
            MethodCallExpression methodCall = expression as MethodCallExpression;
            if (methodCall == null)
            {
                return false;
            }

            // Make sure that the right method ie being called
            if (methodCall.Method != ConstraintServices._metadataContainsKeyMethod)
            {
                return false;
            }

            // Make sure the method is being called on the right object            
            MemberExpression targetMember = methodCall.Object as MemberExpression;
            if (targetMember == null)
            {
                return false;
            }

            if ((targetMember.Expression != parameter) || (targetMember.Member != ConstraintServices._exportDefinitionMetadataProperty))
            {
                return false;
            }

            // There should only ever be one argument; otherwise, 
            // we've got the wrong IDictionary.ContainsKey method.
            Assumes.IsTrue(methodCall.Arguments.Count == 1);

            // Argument should a constant expression containing the metadata key
            ConstantExpression requiredMetadataConstant = methodCall.Arguments[0] as ConstantExpression;
            if (requiredMetadataConstant == null)
            {
                return false;
            }

            if (!TryParseStringConstant(requiredMetadataConstant, out requiredMetadataName))
            {
                return false;
            }

            return true;
        }

        private static bool TryParseStringConstant(ConstantExpression constant, out string result)
        {
            Assumes.NotNull(constant);

            if (constant.Type == typeof(string) && constant.Value != null)
            {
                result = (string)constant.Value;
                return true;
            }

            result = null;
            return false;
        }

        public static Expression<Func<T, bool>> AndAlso<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
        {
            Assumes.NotNull(left);
            if (right == null)
                return left;

            ParameterExpression parameter = Expression.Parameter(left.Parameters[0].Type, left.Parameters[0].Name);
            Expression body = Expression.AndAlso(Expression.Invoke(left, parameter), Expression.Invoke(right, parameter));
            return Expression.Lambda<Func<T, bool>>(body, parameter);
        }

        private static Expression<Func<T1, T3>> Compose<T1, T2, T3>(this Expression<Func<T1, T2>> f1, Expression<Func<T2, T3>> f2)
        {
            return Expression.Lambda<Func<T1, T3>>(
                Expression.Invoke(
                    (LambdaExpression)f2,
                    new Expression[] { f1.Body }),
                f1.Parameters);
        }

        public static IQueryable<T1> CompositeWhere<T1, T2>(this IQueryable<T1> sequence, Expression<Func<T1, T2>> converter, Expression<Func<T2, bool>> criteria)
        {
            return sequence.Where(converter.Compose(criteria));
        }
    }
}
